/*
 * Copyright 2007 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import com.google.javascript.jscomp.CompilerOptions.AliasStringsMode;
import com.google.javascript.jscomp.testing.JSChunkGraphBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link AliasStrings}. */
@RunWith(JUnit4.class)
public final class AliasStringsTest extends CompilerTestCase {

  private static final String EXTERNS = "alert";

  private boolean hashReduction = false;

  private AliasStringsMode aliasStringsMode = AliasStringsMode.ALL;

  public AliasStringsTest() {
    super(EXTERNS);
  }

  @Override
  protected CompilerPass getProcessor(Compiler compiler) {
    AliasStrings pass =
        new AliasStrings(compiler, compiler.getChunkGraph(), false, aliasStringsMode);
    if (hashReduction) {
      pass.unitTestHashReductionMask = 0;
    }
    return pass;
  }

  @Test
  public void testTemplateLiteral() {
    // TODO(bradfordcsmith): Maybe implement using aliases in template literals?
    test(
        lines(
            "const A = 'aliasable string';",
            "const B = 'aliasable string';",
            "const AB = `${A}aliasable string${B}`;"),
        lines(
            "var $$S_aliasable$20string = 'aliasable string';",
            "const A = $$S_aliasable$20string;",
            "const B = $$S_aliasable$20string;",
            "const AB = `${A}aliasable string${B}`"));
  }

  @Test
  public void testProtectedMessage() {
    test(
        lines(
            "const A = 'aliasable string';",
            "const B = 'aliasable string';",
            "var MSG_A =",
            "    " + ReplaceMessagesConstants.DEFINE_MSG_CALLEE + "(",
            "        {",
            "          \"key\":    \"MSG_A\",",
            "          \"msg_text\":\"aliasable string\",",
            "        });",
            ""),
        lines(
            "var $$S_aliasable$20string = 'aliasable string';",
            "const A = $$S_aliasable$20string;",
            "const B = $$S_aliasable$20string;",
            "var MSG_A =",
            "    " + ReplaceMessagesConstants.DEFINE_MSG_CALLEE + "(",
            "        {",
            "          \"key\":    \"MSG_A\",",
            // This string is left unmolested instead of using an alias,
            // because `ReplaceMessages` needs the literal string here.
            "          \"msg_text\":\"aliasable string\",",
            "        });",
            ""));
  }

  @Test
  public void testLongStableAlias() {
    // Check long strings get a hash code

    test(
        "a='Antidisestablishmentarianism';" + "b='Antidisestablishmentarianism';",
        "var $$S_Antidisestablishment_e428eaa9="
            + "  'Antidisestablishmentarianism';"
            + "a=$$S_Antidisestablishment_e428eaa9;"
            + "b=$$S_Antidisestablishment_e428eaa9");

    // Check that small changes give different hash codes

    test(
        "a='AntidisestablishmentarianIsm';" + "b='AntidisestablishmentarianIsm';",
        "var $$S_Antidisestablishment_e4287289="
            + "  'AntidisestablishmentarianIsm';"
            + "a=$$S_Antidisestablishment_e4287289;"
            + "b=$$S_Antidisestablishment_e4287289");

    // TODO(user): check that hash code collisions are handled.
  }

  @Test
  public void testLongStableAliasHashCollision() {
    hashReduction = true;

    // Check that hash code collisions generate different alias
    // variable names

    test(
        "f('Antidisestablishmentarianism');"
            + "f('Antidisestablishmentarianism');"
            + "f('Antidisestablishmentarianismo');"
            + "f('Antidisestablishmentarianismo');",
        "var $$S_Antidisestablishment_0="
            + "  'Antidisestablishmentarianism';"
            + "var $$S_Antidisestablishment_0_1="
            + "  'Antidisestablishmentarianismo';"
            + "f($$S_Antidisestablishment_0);"
            + "f($$S_Antidisestablishment_0);"
            + "f($$S_Antidisestablishment_0_1);"
            + "f($$S_Antidisestablishment_0_1);");
  }

  @Test
  public void testStringsThatAreGlobalVarValues() {

    testSame("var foo='foo'; var bar='';");

    // Regular array
    testSame("var foo=['foo','bar'];");

    // Nested array
    testSame("var foo=['foo',['bar']];");

    // Same string is in a global array and a local in a function
    testSame("var foo=['foo', 'bar'];function bar() {return 'foo';}");

    // Regular object literal
    testSame("var foo={'foo': 'bar'};");

    // Nested object literal
    testSame("var foo={'foo': {'bar': 'baz'}};");

    // Same string is in a global object literal (as key) and local in a
    // function
    testSame("var foo={'foo': 'bar'};function bar() {return 'foo';}");

    // Same string is in a global object literal (as value) and local in a
    // function
    testSame("var foo={'foo': 'foo'};function bar() {return 'foo';}");
  }

  @Test
  public void testStringsInModules() {

    // Aliases must be placed in the correct chunk. The alias for
    // '------adios------' must be lifted from m2 and m3 and go in the
    // common parent chunks m1

    JSChunk[] chunks =
        JSChunkGraphBuilder.forBush()
            .addChunk(
                "function f(a) { alert('ffffffffffffffffffff' + 'ffffffffffffffffffff' + a); }"
                    + "function g() { alert('ciaociaociaociaociao'); }")
            .addChunk(
                "f('---------hi---------');f('bye');function h(a) { alert('hhhhhhhhhhhhhhhhhhhh'"
                    + " + 'hhhhhhhhhhhhhhhhhhhh' + a); }")
            .addChunk(
                "f('---------hi---------');h('ciaociaociaociaociao' +"
                    + " '--------adios-------');(function() { alert('zzzzzzzzzzzzzzzzzzzz' +"
                    + " 'zzzzzzzzzzzzzzzzzzzz'); })();")
            .addChunk(
                "f('---------hi---------'); alert('--------adios-------');"
                    + "h('-------peaches------'); h('-------peaches------');")
            .build();

    test(
        srcs(chunks),
        expected(
            // m1
            "var $$S_ciaociaociaociaociao = 'ciaociaociaociaociao';"
                + "var $$S_ffffffffffffffffffff = 'ffffffffffffffffffff';"
                + "function f(a) { "
                + "  alert($$S_ffffffffffffffffffff + $$S_ffffffffffffffffffff + a); "
                + "}"
                + "function g() { alert($$S_ciaociaociaociaociao); }",
            // m2
            "var $$S_$2d$2d$2d$2d$2d$2d$2d$2d$2dhi$2d$2d$2d$2d$2d$2d$2d$2d$2d"
                + " = '---------hi---------';"
                + "var $$S_$2d$2d$2d$2d$2d$2d$2d$2d_adios$2d$2d$2d$2d$2d$2d$2d"
                + " = '--------adios-------'; "
                + "var $$S_hhhhhhhhhhhhhhhhhhhh = 'hhhhhhhhhhhhhhhhhhhh';"
                + "f($$S_$2d$2d$2d$2d$2d$2d$2d$2d$2dhi$2d$2d$2d$2d$2d$2d$2d$2d$2d);"
                + "f('bye');"
                + "function h(a) {"
                + "  alert($$S_hhhhhhhhhhhhhhhhhhhh + $$S_hhhhhhhhhhhhhhhhhhhh + a);"
                + "}",
            // m3
            "var $$S_zzzzzzzzzzzzzzzzzzzz = 'zzzzzzzzzzzzzzzzzzzz';"
                + "f($$S_$2d$2d$2d$2d$2d$2d$2d$2d$2dhi$2d$2d$2d$2d$2d$2d$2d$2d$2d);"
                + "h($$S_ciaociaociaociaociao + "
                + "$$S_$2d$2d$2d$2d$2d$2d$2d$2d_adios$2d$2d$2d$2d$2d$2d$2d);"
                + "(function() { alert($$S_zzzzzzzzzzzzzzzzzzzz + $$S_zzzzzzzzzzzzzzzzzzzz) })();",
            // m4
            "var $$S_$2d$2d$2d$2d$2d$2d$2dpeaches$2d$2d$2d$2d$2d$2d"
                + " = '-------peaches------';"
                + "f($$S_$2d$2d$2d$2d$2d$2d$2d$2d$2dhi$2d$2d$2d$2d$2d$2d$2d$2d$2d);"
                + "alert($$S_$2d$2d$2d$2d$2d$2d$2d$2d_adios$2d$2d$2d$2d$2d$2d$2d);"
                + "h($$S_$2d$2d$2d$2d$2d$2d$2dpeaches$2d$2d$2d$2d$2d$2d);"
                + "h($$S_$2d$2d$2d$2d$2d$2d$2dpeaches$2d$2d$2d$2d$2d$2d);"));
  }

  @Test
  public void testStringsInModules2() {

    // Aliases must be placed in the correct module. The alias for
    // '------adios------' must be lifted from m2 and m3 and go in the
    // common parent module m1

    JSChunk[] chunks =
        JSChunkGraphBuilder.forBush()
            .addChunk("function g() { alert('ciaociaociaociaociao'); }")
            .addChunk(
                "function h(a) {"
                    + "  alert('hhhhhhhhhhhhhhhhhhh:' + a);"
                    + "  alert('hhhhhhhhhhhhhhhhhhh:' + a);"
                    + "}")
            .addChunk("h('ciaociaociaociaociao' + 'adios');")
            .addChunk("g();")
            .build();

    test(
        srcs(chunks),
        expected(
            // m1
            lines(
                "var $$S_ciaociaociaociaociao = 'ciaociaociaociaociao';",
                "function g() { alert($$S_ciaociaociaociaociao); }"),
            // m2
            lines(
                "var $$S_hhhhhhhhhhhhhhhhhhh$3a = 'hhhhhhhhhhhhhhhhhhh:';",
                "function h(a) {"
                    + "  alert($$S_hhhhhhhhhhhhhhhhhhh$3a + a);"
                    + "  alert($$S_hhhhhhhhhhhhhhhhhhh$3a + a);"
                    + "}"),
            // m3
            "h($$S_ciaociaociaociaociao + 'adios');",
            // m4
            "g();"));
  }

  @Test
  public void testAliasInCommonModuleInclusive() {

    JSChunk[] chunks =
        JSChunkGraphBuilder.forBush()
            .addChunk("")
            .addChunk("function g() { alert('ciaociaociaociaociao'); }")
            .addChunk("h('ciaociaociaociaociao' + 'adios');")
            .addChunk("g();")
            .build();

    // The "ciao" string is used in m1 and m2.
    // Since m2 depends on m1, we should create the module there and not force it into m0.
    test(
        srcs(chunks),
        expected(
            // m0
            "",
            // m1
            lines(
                "var $$S_ciaociaociaociaociao = 'ciaociaociaociaociao';",
                "function g() { alert($$S_ciaociaociaociaociao); }"),
            // m2
            "h($$S_ciaociaociaociaociao + 'adios');",
            // m3
            "g();"));
  }

  @Test
  public void testEmptyModules() {
    JSChunk[] chunks =
        JSChunkGraphBuilder.forStar()
            .addChunk("")
            .addChunk("function foo() { f('goodgoodgoodgoodgood') }")
            .addChunk("function foo() { f('goodgoodgoodgoodgood') }")
            .build();

    test(
        srcs(chunks),
        expected(
            // m0
            "var $$S_goodgoodgoodgoodgood='goodgoodgoodgoodgood'",
            // m1
            "function foo() {f($$S_goodgoodgoodgoodgood)}",
            // m2
            "function foo() {f($$S_goodgoodgoodgoodgood)}"));
  }

  @Test
  public void testOnlyAliasLargeStrings() {
    aliasStringsMode = AliasStringsMode.LARGE;

    test(
        lines(
            "const A = 'non aliasable string with length <= 100 characters';",
            "const B = 'non aliasable string with length <= 100 characters';",
            // C and D have lengths of 101 characters
            "const C = 'aliasable large string"
                + " largestringlargestringlargestringlargestringlargestringlargestringlargestring!';",
            "const D = 'aliasable large string"
                + " largestringlargestringlargestringlargestringlargestringlargestringlargestring!';"),
        lines(
            "var $$S_aliasable$20large$20stri_6c7cf169 = 'aliasable large string"
                + " largestringlargestringlargestringlargestringlargestringlargestringlargestring!';",
            "const A = 'non aliasable string with length <= 100 characters';",
            "const B = 'non aliasable string with length <= 100 characters';",
            "const C = $$S_aliasable$20large$20stri_6c7cf169;",
            "const D = $$S_aliasable$20large$20stri_6c7cf169;"));
  }
}
